package main

import (
	"errors"
	"fmt"
	"gitee.com/geektime-geekbang/geektime-go/advance/template/gen/annotation"
	"gitee.com/geektime-geekbang/geektime-go/advance/template/gen/http"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"unicode"
)

// 实际上 main 函数这里要考虑接收参数
// src 源目标
// dst 目标目录
// type src 里面可能有很多类型，那么用户可能需要指定具体的类型
// 这里我们简化操作，只读取当前目录下的数据，并且扫描下面的所有源文件，然后生成代码
// 在当前目录下运行 go install 就将 main 安装成功了，
// 可以在命令行中运行 gen
// 在 testdata 里面运行 gen，则会生成能够通过所有测试的代码
func main() {
	err := gen(".")
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fmt.Println("success")
}

func gen(src string) error {
	// 第一步找出符合条件的文件
	srcFiles, err := scanFiles(src)
	if err != nil {
		return err
	}
	// 第二步，AST 解析源代码文件，拿到 service definition 定义
	defs, err := parseFiles(srcFiles)
	if err != nil {
		return err
	}
	// 生成代码
	return genFiles(src, defs)
}

// 根据 defs 来生成代码
// src 是源代码所在目录，在测试里面它是 ./testdata
func genFiles(src string, defs []http.ServiceDefinition) error {

	for _, def := range defs {

		file, err := os.Create(filepath.Join(src, underscoreName(def.Name+"Gen")+".go"))
		if err != nil {
			return err
		}

		err = http.Gen(file, def)

		if err != nil {
			return err
		}

		err = file.Close()

		if err != nil {
			return err
		}
	}

	return nil

	//panic("implement me")
}

func parseFiles(srcFiles []string) ([]http.ServiceDefinition, error) {
	defs := make([]http.ServiceDefinition, 0, 20)
	for _, src := range srcFiles {
		fmt.Println(src)
		// 你需要利用 annotation 里面的东西来扫描 src，然后生成 file
		//panic("implement me")
		//var file annotation.File

		fset := token.NewFileSet()

		fAst, err := parser.ParseFile(fset, src, nil, parser.ParseComments)
		if err != nil {
			return nil, err
		}

		fileWalk := &annotation.SingleFileEntryVisitor{}

		ast.Walk(fileWalk, fAst)

		file := fileWalk.Get()

		for _, typ := range file.Types {
			_, ok := typ.Annotations.Get("HttpClient")
			if !ok {
				continue
			}
			def, err := parseServiceDefinition(file.Node.Name.Name, typ)
			if err != nil {
				return nil, err
			}
			defs = append(defs, def)
		}
	}
	return defs, nil
}

// 你需要利用 typ 来构造一个 http.ServiceDefinition
// 注意你可能需要检测用户的定义是否符合你的预期
func parseServiceDefinition(pkg string, typ annotation.Type) (http.ServiceDefinition, error) {
	//panic("implement me")

	srvName := typ.Node.Name.Name

	if anno, ok := typ.Get("ServiceName"); ok {
		srvName = anno.Value
	}

	methods := make([]http.ServiceMethod, 0, len(typ.Fields))

	for _, field := range typ.Fields {
		//field.Node
		//field.Ans

		reqPath := "/" + field.Node.Names[0].Name

		if anno, ok := field.Get("Path"); ok {
			reqPath = anno.Value
		}

		ft, ok := field.Node.Type.(*ast.FuncType)

		if !ok {
			continue
		}

		fpList := ft.Params.List

		if len(fpList) != 2 {
			return http.ServiceDefinition{}, errors.New("gen: 方法必须接收两个参数，其中第一个参数是 context.Context，第二个参数请求")
		}

		frList := ft.Results.List

		if len(frList) != 2 {
			return http.ServiceDefinition{}, errors.New("gen: 方法必须返回两个参数，其中第一个返回值是响应，第二个返回值是error")
		}

		fpStar, ok := fpList[1].Type.(*ast.StarExpr)

		if !ok {
			return http.ServiceDefinition{}, errors.New("gen: 方法必须接收两个参数，其中第一个参数是 context.Context，第二个参数请求必须是指针")
		}

		fpId, ok := fpStar.X.(*ast.Ident)

		if !ok {
			return http.ServiceDefinition{}, errors.New("gen: 方法必须接收两个参数，其中第一个参数是 context.Context，第二个参数请求必须是指针")
		}

		frStar, ok := frList[0].Type.(*ast.StarExpr)

		if !ok {
			return http.ServiceDefinition{}, errors.New("gen: 方法必须返回两个参数，其中第一个返回值是响应必须是指针，第二个返回值是error")
		}

		frId, ok := frStar.X.(*ast.Ident)

		if !ok {
			return http.ServiceDefinition{}, errors.New("gen: 方法必须返回两个参数，其中第一个返回值是响应必须是指针，第二个返回值是error")
		}

		method := http.ServiceMethod{
			Name:         field.Node.Names[0].Name,
			Path:         reqPath,
			ReqTypeName:  fpId.Name,
			RespTypeName: frId.Name,
		}

		methods = append(methods, method)
	}

	sd := http.ServiceDefinition{
		Package: pkg,
		Name:    srvName,
		Methods: methods,
	}

	return sd, nil
}

// 返回符合条件的 Go 源代码文件，也就是你要用 AST 来分析这些文件的代码
func scanFiles(src string) ([]string, error) {

	fPaths := make([]string, 0, 8)

	absSrc, err := filepath.Abs(src)
	if err != nil {
		return nil, err
	}

	err = walkDir(absSrc, func(str string) error {
		if path.Ext(str) != ".go" {
			return nil
		}

		if isHttpClientFile(str) {
			fPaths = append(fPaths, str)
		}

		return nil
	})
	if err != nil {
		return nil, err
	}
	return fPaths, nil
	//panic("implement me")
}

func walkDir(src string, walk func(str string) error) error {
	entries, err := os.ReadDir(src)
	if err != nil {
		return err
	}
	for _, entry := range entries {
		fPath := filepath.Join(src, entry.Name())
		if entry.IsDir() {
			err = walkDir(fPath, walk)
			if err != nil {
				return err
			}
			continue
		}

		err = walk(fPath)
		if err != nil {
			return err
		}
	}
	return nil
}

var httpClientReg = regexp.MustCompile("(?i)//\\s+@httpclient")

func isHttpClientFile(fPath string) bool {

	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, fPath, nil, parser.ParseComments)
	if err != nil {
		return false
	}
	for _, decl := range file.Decls {
		dl, ok := decl.(*ast.GenDecl)
		if !ok {
			continue
		}
		if dl.Tok != token.TYPE {
			continue
		}
		for _, spec := range dl.Specs {
			ts, ok := spec.(*ast.TypeSpec)
			if !ok {
				continue
			}
			_, ok = ts.Type.(*ast.InterfaceType)
			if !ok {
				continue
			}
			for _, comment := range ts.Doc.List {
				if httpClientReg.MatchString(comment.Text) {
					return true
				}
			}
		}
	}
	return false
}

// underscoreName 驼峰转字符串命名，在决定生成的文件名的时候需要这个方法
// 可以用正则表达式，然而我写不出来，我是正则渣
func underscoreName(name string) string {
	var buf []byte
	for i, v := range name {
		if unicode.IsUpper(v) {
			if i != 0 {
				buf = append(buf, '_')
			}
			buf = append(buf, byte(unicode.ToLower(v)))
		} else {
			buf = append(buf, byte(v))
		}

	}
	return string(buf)
}
